﻿using System;
using System.Data;
using System.Data.SqlServerCe;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using OverStore.Configuration;
using OverStore.Configuration.Components;
using OverStore.Configuration.Builder;
using OverStore.Sql;
using OverStore.Sql.TableSchema;
using System.Reflection;
using OverStore.Configuration.Builder.Fluent;
using OverStore.Runtime.Storage;
using OverStore.Runtime.ObjectRepository;
using OverStore.Runtime.ObjectRepository.Mapping;
using OverStore.Runtime.ObjectRepository.CommandSet;
using OverStore.Configuration.ObjectRepository;

namespace OverStore.TutorialAsTest.Tutorial1.Lesson4
{
    [TestClass]
    public class Lesson4
    {
        /*******
         * In early lessons we considered reading of simple objects,
         * sequenses of simple objects and collection of simple objects.
         * 
         * In this lesson we start to consider composite objects.
         * CompositeWithAutoId object is object contains references to other objects 
         * or set of objects loaded via OverStore.
         * 
         * So, we have two conditions when simple object become composite object:
         * 1. Condition of requirement: Object contains references to other objects.
         * 2. Condition of sufficiency: References should be processed by OverStore.
         * 
         * Let me note important thing:
         * ** References is not natural model for relational storage (RDBMS).**
         * And conclusion from this thing:
         * ** It is always slower than reference-less model. **
         * 
         * There are many conditions and factors affects performance decreasing:
         * These are some of they:
         *  1. Number of references (it is obvious factor :) )
         *  2. OverrideDbType of references 
         *  3. Number of variants to reference (how many object may be referenced,
         *      and how many objects are actually referenced).
         *      
         * It is two type of references exists:
         * 1. 'Foreing keys'. In database represented as ... foreign keys, yes. 
         *  But it is simple RELATION. Field that contains primary key of another table.
         *  It may be represented in object in few ways: at first, this result may be simple 
         *  ignored if it is not required. At second, it may consist in object as result. 
         *  And at third, it may be references to object represents row in foreign table.
         *  
         * 2. Assosiations. Actually, it may be collection of object that reference current 
         * object. It may be any object or any collection of object.
         * 
         * References always increases amount of information required for loading. Any 
         * object composition granulate storage queries. 
         * 
         * In this lesson we intentionally consider only references, because assosiations is
         * more wide subject and we consider it later after saving composite objects.
         * 
         * We will use Item table, ItemType table and UserAccount table for initially 
         * demonstrating composite object loading
         * 
         * We will use almost all data from Item table
         * 
         * Id                                   OverrideDbType    Name        CreatedBy                               CreatedDate         EditedBy                                EditedDate          DeletedBy   DeletedDate
         * ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
         * b7b0b51b-05ee-41a8-9085-6cd70fe91d1f	2	    USD	        c0a23bf2-8266-440c-95e5-7ce2830687e2	22.05.2009 0:00:00	c0a23bf2-8266-440c-95e5-7ce2830687e2	22.05.2009 0:00:00	NULL	    NULL
         * d7c16782-12e1-4519-b15e-266fa60be0da	2	    UAH	        af7989bb-1acb-4d00-a49a-03551c3857cc	26.05.2009 0:00:00	af7989bb-1acb-4d00-a49a-03551c3857cc	26.05.2009 0:00:00	NULL	    NULL
         * ed84a0c5-47c1-414c-b68b-69d338a3d830	1	    Meat	    af7989bb-1acb-4d00-a49a-03551c3857cc	26.05.2009 0:00:00	af7989bb-1acb-4d00-a49a-03551c3857cc	26.05.2009 0:00:00	NULL	    NULL
         * 8361e80f-76e5-4832-be7f-e55c761ba5c4	1	    Bread	    af7989bb-1acb-4d00-a49a-03551c3857cc	26.05.2009 0:00:00	af7989bb-1acb-4d00-a49a-03551c3857cc	26.05.2009 0:00:00	NULL	    NULL
         * 5136773a-eca9-4daf-a7a6-52ef443ef6d5	1	    Milk	    af7989bb-1acb-4d00-a49a-03551c3857cc	26.05.2009 0:00:00	af7989bb-1acb-4d00-a49a-03551c3857cc	26.05.2009 0:00:00	NULL	    NULL
         * a0a9a28e-979f-483b-9467-b95cc41fe31c	1	    Vegetables	af7989bb-1acb-4d00-a49a-03551c3857cc	26.05.2009 0:00:00	af7989bb-1acb-4d00-a49a-03551c3857cc	26.05.2009 0:00:00	NULL	    NULL
         * 0fee8f08-51ec-4d44-81e8-c3ac2898340b	1	    Bear	    af7989bb-1acb-4d00-a49a-03551c3857cc	26.05.2009 0:00:00	af7989bb-1acb-4d00-a49a-03551c3857cc	26.05.2009 0:00:00	NULL	    NULL
         * 82f1492a-d4c8-4f46-a223-70d6e68a5da5	1	    Viski	    af7989bb-1acb-4d00-a49a-03551c3857cc	26.05.2009 0:00:00	af7989bb-1acb-4d00-a49a-03551c3857cc	26.05.2009 0:00:00	NULL	    NULL
         * f81e87b0-4537-453f-9e70-26e58b361ee0	1	    Absent	    af7989bb-1acb-4d00-a49a-03551c3857cc	26.05.2009 0:00:00	af7989bb-1acb-4d00-a49a-03551c3857cc	26.05.2009 0:00:00	NULL	    NULL
         * e718725c-9605-40d2-be06-1be41a51d9dc	1	    Marlboro	af7989bb-1acb-4d00-a49a-03551c3857cc	26.05.2009 0:00:00	af7989bb-1acb-4d00-a49a-03551c3857cc	26.05.2009 0:00:00	NULL	    NULL
         * 9838eef2-6c71-4fdd-9ea9-eea02e3fd723	1	    Underwear	af7989bb-1acb-4d00-a49a-03551c3857cc	26.05.2009 0:00:00	af7989bb-1acb-4d00-a49a-03551c3857cc	26.05.2009 0:00:00	NULL	    NULL
         * 8075e129-24e9-4833-9a53-f1cd58bd9fc7	1	    Warm wear	af7989bb-1acb-4d00-a49a-03551c3857cc	26.05.2009 0:00:00	af7989bb-1acb-4d00-a49a-03551c3857cc	26.05.2009 0:00:00	NULL	    NULL
         * 56c218ea-f396-48a4-a092-3b354b97efc0	1	    Shorts	    af7989bb-1acb-4d00-a49a-03551c3857cc	26.05.2009 0:00:00	af7989bb-1acb-4d00-a49a-03551c3857cc	26.05.2009 0:00:00	NULL	    NULL
         * 0570a74f-9c96-45c5-9924-e7597ec88991	1	    Cinema	    af7989bb-1acb-4d00-a49a-03551c3857cc	26.05.2009 0:00:00	af7989bb-1acb-4d00-a49a-03551c3857cc	26.05.2009 0:00:00	NULL	    NULL
         * 3879c67c-82a6-44b4-8492-85b645de2d4c	1	    Bar	        af7989bb-1acb-4d00-a49a-03551c3857cc	26.05.2009 0:00:00	af7989bb-1acb-4d00-a49a-03551c3857cc	26.05.2009 0:00:00	NULL	    NULL
         * 
         * 
         * And part of data (for simplicity) from UserAccount table.
         * 
         * Id                                   Login   PasswordHash                        Name            CreatedBy
         * --------------------------------------------------------------------------------------------------------------------------------------
         * c0a23bf2-8266-440c-95e5-7ce2830687e2	system	F38C96D1ED81436CAFE2E962DA6ABBDF	System Account  c0a23bf2-8266-440c-95e5-7ce2830687e2
         * 6a148282-8a18-4b72-b577-cf2f1005df65	admin	admin	                            Administrator   c0a23bf2-8266-440c-95e5-7ce2830687e2
         * af7989bb-1acb-4d00-a49a-03551c3857cc	sergey	pladder	                            Sergey Tokarev  c0a23bf2-8266-440c-95e5-7ce2830687e2
         * 
         * 
         * ItemType table. Very small and simply.
         * 
         * OverrideDbType   Name
         * ---------------------------
         * 1	Product or Service
         * 2	Currency
         * 
         * 
         *****/

        class ItemType
        {
            public int Id { get; set; }

            public string Name { get; set; }
        }

        static IComponentConfiguration<IMapper<ItemType>> CreateItemTypeMapper()
        {
            return Component.Mapper.Delegate<ItemType>(
                (item, name, p) =>
                {
                    if (name == "Type")
                        return item.Id;
                    if (name == "Name")
                        return item.Name;
                    if (name == "Description")
                        return null;
                    if (name == "orig__Type")
                        return p.GetIdentifier<ItemType, int>(item);
                    return Missing.Value;
                },
                (data, item, p) =>
                {
                    item.Name = data.GetString("Name");
                });
        }

        class UserAccount
        {
            public Guid Id { get; set; }

            public string Login { get; set; }

            public string PasswordHash { get; set; }

            public string Name { get; set; }

            public UserAccount CreatedBy { get; set; }
        }

        static IComponentConfiguration<IMapper<UserAccount>> CreateUserAccountMapper()
        {
            return Component.Mapper.Delegate<UserAccount>(
                (item, name, p) =>
                {
                    if (name == "Id")
                        return item.Id;
                    if (name == "Login")
                        return item.Login;
                    if (name == "PasswordHash")
                        return item.PasswordHash;
                    if (name == "Name")
                        return item.Name;
                    if (name == "CreatedBy")
                        return item.CreatedBy == null ? null : (object)p.GetIdentifier<UserAccount, Guid>(item.CreatedBy);
                    if (name == "orig__Id")
                        return p.GetIdentifier<UserAccount, Guid>(item);
                    return Missing.Value;
                },
                (data, item, p) =>
                {
                    var cb = data.GetNullableGuid("CreatedBy");
                    item.CreatedBy = cb == null ? null : p.GetInstance<UserAccount, Guid>(cb.Value);
                    item.Login = data.GetString("Login");
                    item.Name = data.GetString("Name");
                    item.PasswordHash = data.GetString("PasswordHash");
                });
        }

        static IComponentConfiguration<ICommandSet> CreateUserAccountCommandSet()
        {
            return Component.CommandSet.FromTable(
                Component.CreateTable("UserAccount")
                                  .Key<Guid>("Id")
                                  .Column<String>("Login")
                                  .Column<String>("PasswordHash")
                                  .Column<String>("Name")
                                  .Column<Guid?>("CreatedBy"), true);
        }

        class Item
        {
            public Guid Id { get; set; }

            public ItemType Type { get; set; }

            public string Name { get; set; }

            public UserAccount CreatedBy { get; set; }

            public DateTime CreatedDate { get; set; }

            public UserAccount EditedBy { get; set; }

            public DateTime EditedDate { get; set; }

            public UserAccount DeletedBy { get; set; }

            public DateTime? DeletedDate { get; set; }
        }

        static IComponentConfiguration<IMapper<Item>> CreateItemMapper()
        {
            return Component.Mapper.Delegate<Item>(
                (item, name, p) =>
                {
                    if (name == "Id")
                        return item.Id;
                    if (name == "Type")
                        return p.GetIdentifier<ItemType, int>(item.Type);
                    if (name == "Name")
                        return item.Name;
                    if (name == "CreatedBy")
                        return item.CreatedBy == null ? null : (object)p.GetIdentifier<UserAccount, Guid>(item.CreatedBy);
                    if (name == "CreatedDate")
                        return item.CreatedDate;
                    if (name == "EditedBy")
                        return item.EditedBy == null ? null : (object)p.GetIdentifier<UserAccount, Guid>(item.EditedBy);
                    if (name == "EditedDate")
                        return item.EditedDate;
                    if (name == "DeletedBy")
                        return item.DeletedBy == null ? null : (object)p.GetIdentifier<UserAccount, Guid>(item.DeletedBy);
                    if (name == "DeletedDate")
                        return item.DeletedDate;
                    if (name == "orig__Id")
                        return p.GetIdentifier<Item, Guid>(item);
                    return Missing.Value;
                },
                (data, item, p) =>
                {
                    item.Type = p.GetInstance<ItemType, int>(data.GetInt32("Type"));
                    item.Name = data.GetString("Name");
                    var cb = data.GetNullableGuid("CreatedBy");
                    item.CreatedBy = cb == null ? null : p.GetInstance<UserAccount, Guid>(cb.Value);
                    item.CreatedDate = data.GetDateTime("CreatedDate");
                    var eb = data.GetNullableGuid("EditedBy");
                    item.EditedBy = eb == null ? null : p.GetInstance<UserAccount, Guid>(eb.Value);
                    item.EditedDate = data.GetDateTime("EditedDate");
                    var db = data.GetNullableGuid("DeletedBy");
                    item.DeletedBy = db == null ? null : p.GetInstance<UserAccount, Guid>(db.Value);
                    item.DeletedDate = data.GetNullableDateTime("DeletedDate");
                });
        }

        static IComponentConfiguration<ICommandSet> CreateItemCommandSet()
        {
            return Component.CommandSet.FromTable(
                    Component.CreateTable("Item")
                        .Key<Guid>("Id")
                        .Column<int>("Type")
                        .Column<string>("Name")
                        .Column<Guid>("CreatedBy")
                        .Column<DateTime>("CreatedDate")
                        .Column<Guid>("EditedBy")
                        .Column<DateTime>("EditedDate")
                        .Column<Guid?>("DeletedBy")
                        .Column<DateTime?>("DeletedDate"), true);
        }

        static IComponentConfiguration<IRepositoryBase> CreateItemTypeRepository()
        {
            return new PersistenceObjectRepositoryConfiguration<ItemType, int>
            {
                Commands = Component.CommandSet.FromTable(
                                        Component.CreateTable("ItemType")
                                                 .Key<int>("Type")
                                                 .Column<string>("Name"),
                                        false),
                Depot = Component.Depot.WithInternalKey<ItemType, int>("Id"),
                Mapper = CreateItemTypeMapper()
            };
        }

        static OverStoreConfiguration CreateConfig(string connectionString)
        {
            return OverStoreFluent.Configure()
                .LogToConsole().ConnectToDatabase<SqlCeConnection>(connectionString, ProcessOpenedReadersMode.ForciblyCloseReaders)
                .AddRepository(CreateItemTypeRepository())
                .CustomClassRepository<UserAccount, Guid>()
                    .WithDepot(Component.Depot.WithInternalKey<UserAccount, Guid>("Id"))
                    .WithMapper(CreateUserAccountMapper())
                    .WithCommands(CreateUserAccountCommandSet())
                    .ByIdentifierAttaching(i => i.Id)
                .EndObjectRepository()
                .CustomClassRepository<Item, Guid>()
                    .WithDepot(Component.Depot.WithInternalKey<Item, Guid>("Id"))
                    .WithMapper(CreateItemMapper())
                    .WithCommands(CreateItemCommandSet())
                    .ByIdentifierAttaching(i => i.Id)
                .EndObjectRepository()
            .End();
        }

        [TestMethod]
        public void CompositeObjectGetItemType()
        {
            var connectionString = TestHelper.CreateMyMoneyCopyAndBuildConnectionString();
            using (var session = CreateConfig(connectionString).CreateSession())
            {
                var itemType = session.GetByIdentifier<ItemType, int>(1);

                Assert.AreEqual(1, itemType.Id);
                Assert.AreEqual("Product or Service", itemType.Name);
            }
        }

        [TestMethod]
        public void CompositeObjectGetUserAccount()
        {
            var connectionString = TestHelper.CreateMyMoneyCopyAndBuildConnectionString();
            using (var session = CreateConfig(connectionString).CreateSession())
            {
                var id = new Guid("c0a23bf2-8266-440c-95e5-7ce2830687e2");
                var ua = session.GetByIdentifier<UserAccount, Guid>(id);
                Assert.AreEqual(id, ua.Id);
                Assert.AreEqual("system", ua.Login);
                Assert.AreEqual("F38C96D1ED81436CAFE2E962DA6ABBDF", ua.PasswordHash);
                Assert.AreEqual("System Account", ua.Name);
            }
        }

        /// <summary>
        /// This test shows identity mapping for composite objects.
        /// OverStore ORM maintain internal map that allows 
        /// create and load object for each row only once.
        /// 
        /// Out composite object Item has few reference property. Some 
        /// of these properties references to same row, and they should use 
        /// same instance of object as property values.
        /// </summary>
        [TestMethod]
        public void CompositeObjectGetItem()
        {
            var connectionString = TestHelper.CreateMyMoneyCopyAndBuildConnectionString();
            using (var session = CreateConfig(connectionString).CreateSession())
            {
                var currency = session.GetByIdentifier<ItemType, int>(2);

                var id = new Guid("b7b0b51b-05ee-41a8-9085-6cd70fe91d1f");
                var item = session.GetByIdentifier<Item, Guid>(id);


                Assert.AreEqual(id, item.Id);
                Assert.AreEqual("USD", item.Name);
                Assert.AreSame(currency, item.Type);

                var system = session.GetByIdentifier<UserAccount, Guid>(new Guid("c0a23bf2-8266-440c-95e5-7ce2830687e2"));
                Assert.AreSame(system, item.CreatedBy);
                Assert.AreSame(system, item.EditedBy);
                Assert.IsNull(item.DeletedBy);
                Assert.IsNull(item.DeletedDate);
            }
        }

        /// <summary>
        /// This example demonstrates recursive references. 
        /// Recursive reference is closed graph of object.
        /// For example, if object should contains self as result 
        /// of some property.
        /// Or if object should contains object that reference to object 
        /// that reference to instance object.
        /// 
        /// A a = new A();
        /// B b = new B();
        /// C c = new C();
        /// 
        /// a.BRef = b;
        /// b.CRef = c;
        /// c.ARef = a;
        /// 
        /// 
        /// In current test we will use UserAccount class, that contains 
        /// property CreatedBy which also has type UserAccount. This property does not
        /// allows null and has check that referenced user account should exists.
        /// That means at least one row contains reference to self.
        /// 
        /// In our example this it system row.
        /// 
        /// </summary>
        [TestMethod]
        public void CompositeObjectRecursiveReference()
        {
            var connectionString = TestHelper.CreateMyMoneyCopyAndBuildConnectionString();
            using (var session = CreateConfig(connectionString).CreateSession())
            {
                var id = new Guid("c0a23bf2-8266-440c-95e5-7ce2830687e2");
                var system = session.GetByIdentifier<UserAccount, Guid>(id);
                Assert.AreSame(system, system.CreatedBy);
            }
        }
    }
}
